AWS Lambdaカスタムランタイム C++でHTTP APIをつくってみた
こんにちは、CX事業本部のうらわです。
最近、C++の勉強の題材としてAWSが提供しているAWS LambdaカスタムランタイムのC++を触っています。今回はDockerを使用したビルド環境を用意して以下のAPI Gatewayのサンプルコードをビルド・デプロイしてみます。
作業環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H15 $ docker --version Docker version 20.10.2, build 2291f61 $ aws --version aws-cli/2.1.4 Python/3.7.4 Darwin/19.6.0 exe/x86_64
ビルド準備
Lambda関数のコードをLinux環境でビルドする必要があるためDockerを使用します。aws-lambda-cpp-runtime
とaws-sdk-cpp
がインストール済みのDockerfileを用意します。
aws-sdk-cpp
は全サービスのライブラリをビルドするとかなり時間がかかるため、-DBUILD_ONLY="core"
で必要なライブラリを絞っています。
FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ git \ cmake \ make \ g++ \ zip \ libcurl4-openssl-dev \ libssl-dev \ uuid-dev \ zlib1g-dev \ libpulse-dev WORKDIR /tmp RUN git clone https://github.com/awslabs/aws-lambda-cpp-runtime.git && \ cd aws-lambda-cpp-runtime && \ mkdir build && \ cd build && \ cmake .. -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_INSTALL_PREFIX=~/install && \ make && make install RUN git clone https://github.com/aws/aws-sdk-cpp.git && \ cd aws-sdk-cpp && \ mkdir build && \ cd build && \ cmake .. -DBUILD_ONLY="core" \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_UNITY_BUILD=ON \ -DCUSTOM_MEMORY_MANAGEMENT=OFF \ -DCMAKE_INSTALL_PREFIX=~/install \ -DENABLE_UNITY_BUILD=ON && \ make && make install COPY ./run.sh /usr/local/bin/run.sh RUN chmod +x /usr/local/bin/run.sh WORKDIR /src ENTRYPOINT ["run.sh"]
任意のタグを付けてイメージをビルドしておきます。
docker build -t cpp-lambda .
Dockerfile
内でCOPY
しているrun.sh
は以下となります。ENTRYPOINT
に指定しているため、上記でビルドしたDockerイメージを使用してコンテナを起動するとrun.sh
の処理が実行されます。
#! /usr/bin/env bash if [ $# != 1 ]; then echo There is no argument. exit 1 fi mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/install make make $1
Lambda関数の実装
Lambda関数のコードmain.cpp
とビルドするためのCMakeLists.txt
を作成します。とは言っても、GitHubのexamples/api-gatewayのコードそのままです。
mkdir apigw cd apigw touch main.cpp touch CMakeLists.txt
#include <aws/lambda-runtime/runtime.h> #include <aws/core/utils/json/JsonSerializer.h> #include <aws/core/utils/memory/stl/SimpleStringStream.h> using namespace aws::lambda_runtime; invocation_response my_handler(invocation_request const& request) { using namespace Aws::Utils::Json; JsonValue json(request.payload); if (!json.WasParseSuccessful()) { return invocation_response::failure("Failed to parse input JSON", "InvalidJSON"); } auto v = json.View(); Aws::SimpleStringStream ss; ss << "Good "; if (v.ValueExists("body") && v.GetObject("body").IsString()) { auto body = v.GetString("body"); JsonValue body_json(body); if (body_json.WasParseSuccessful()) { auto body_v = body_json.View(); ss << (body_v.ValueExists("time") && body_v.GetObject("time").IsString() ? body_v.GetString("time") : ""); } } ss << ", "; if (v.ValueExists("queryStringParameters")) { auto query_params = v.GetObject("queryStringParameters"); ss << (query_params.ValueExists("name") && query_params.GetObject("name").IsString() ? query_params.GetString("name") : "") << " of "; ss << (query_params.ValueExists("city") && query_params.GetObject("city").IsString() ? query_params.GetString("city") : "") << ". "; } if (v.ValueExists("headers")) { auto headers = v.GetObject("headers"); ss << "Happy " << (headers.ValueExists("day") && headers.GetObject("day").IsString() ? headers.GetString("day") : "") << "!"; } JsonValue resp; resp.WithString("message", ss.str()); return invocation_response::success(resp.View().WriteCompact(), "application/json"); } int main() { run_handler(my_handler); return 0; }
cmake_minimum_required(VERSION 3.5) set(CMAKE_CXX_STANDARD 11) project(api LANGUAGES CXX) find_package(aws-lambda-runtime REQUIRED) find_package(AWSSDK COMPONENTS core) add_executable(${PROJECT_NAME} "main.cpp") target_link_libraries(${PROJECT_NAME} PUBLIC AWS::aws-lambda-runtime ${AWSSDK_LINK_LIBRARIES}) aws_lambda_package_target(${PROJECT_NAME})
Lambda関数のビルド
現在のディレクトリ(main.cpp
とCMakeLists.txt
が存在するapigw
ディレクトリ)をコンテナの/src
ディレクトリにマウントしてDockerコンテナを起動します。実行時の引数に指定しているaws-lambda-package-api
がrun.sh
に渡され、makeコマンドの引数に使用されます。
--rm
オプションをつけているため、このコンテナはビルドが終わったら自動的に削除されます。
docker run --rm -v $(pwd):/src cpp-lambda aws-lambda-package-api
ビルドが成功すると、apigw/build
内にapi.zip
が作成されます。これをLambda関数としてアップロードします。AWS CLIでアップロードする際のコマンド例はGitHubのaws-lambda-cpp-runtime
リポジトリのREADMEに書いてあります。
aws lambda create-function --function-name api \ --role <予めIAMロールを作成しArnを指定する> \ --runtime provided --timeout 15 --memory-size 128 \ --handler api --zip-file fileb://apigw/build/api.zip
API Gatewayの作成とテスト実行
READMEに記載されている通りの手順でAPI Gatewayのリソースを作成します(作成したLambda関数のトリガーからAPI Gatewayを作成します)。
作成が完了したらAPI Gatewayのエンドポイントを確認し、curl
でリクエストを送ってみます。以下のようなメッセージが返ってくれば成功です。
$ curl -X POST \ '<API Gatewayのエンドポイント>/api?name=Bradley&city=Chicago' \ -H 'content-type: application/json' \ -H 'day: Sunday' \ -d '{ "time": "evening" }' {"message":"Good evening, Bradley of Chicago. Happy Sunday!"}
おわりに
ビルドするための環境構築が少々手間ですが、ベースとなるDockerfileさえ用意できればあとは作成するLambda関数に応じて少しカスタマイズするだけになります。
今回はサンプルコードを使用してビルドする手順が中心でした。今後はローカルマシンにもaws-lambda-runtime-cpp
とaws-sdk-cpp
をインストールして自作のLambda関数を実装する準備を整えようと思います。
なお、本記事のコードは以下のリポジトリに格納してあります。